/* eslint-disable */
/*
 * Released under BSD License
 * Copyright (c) 2014-2021 hizzgdev@163.com
 *
 * Project Home:
 *   https://github.com/hizzgdev/jsmind/
 */
(function($w) {
  'use strict'
  // set 'jsMind' as the library name.
  // __name__ should be a const value, Never try to change it easily.
  var __name__ = 'jsMind'
  // library version
  var __version__ = '0.4.6'
  // author
  var __author__ = 'hizzgdev@163.com'

  // an noop function define
  var _noop = function() {
  }
  var logger = (typeof console === 'undefined') ? {
    log: _noop, debug: _noop, error: _noop, warn: _noop, info: _noop
  } : console

  // check global variables
  if (typeof module === 'undefined' || !module.exports) {
    if (typeof $w[__name__] != 'undefined') {
      logger.log(__name__ + ' has been already exist.')
      return
    }
  }

  // shortcut of methods in dom
  var $d = $w.document
  var $g = function(id) {
    return $d.getElementById(id)
  }
  var $c = function(tag) {
    return $d.createElement(tag)
  }
  var $t = function(n, t) {
    if (n.hasChildNodes()) {
      n.firstChild.nodeValue = t
    } else {
      n.appendChild($d.createTextNode(t))
    }
  }

  var $h = function(n, t) {
    if (t instanceof HTMLElement) {
      n.innerHTML = ''
      n.appendChild(t)
    } else {
      n.innerHTML = t
    }
  }
  // detect isElement
  var $i = function(el) {
    return !!el && (typeof el === 'object') && (el.nodeType === 1) && (typeof el.style === 'object') && (typeof el.ownerDocument === 'object')
  }
  if (typeof String.prototype.startsWith != 'function') {
    String.prototype.startsWith = function(p) {
      return this.slice(0, p.length) === p
    }
  }

  var DEFAULT_OPTIONS = {
    container: '',   // id of the container
    editable: false, // you can change it in your options
    theme: null,
    mode: 'full',    // full or side
    support_html: true,

    view: {
      engine: 'canvas',
      hmargin: 10,
      vmargin: 10,
      line_width: 2,
      line_color: '#61BBB4'
    },
    layout: {
      hspace: 30,
      vspace: 20,
      pspace: 6
    },
    default_event_handle: {
      enable_mousedown_handle: true,
      enable_click_handle: true,
      enable_dblclick_handle: true
    },
    shortcut: {
      enable: true,
      handles: {},
      mapping: {
        addchild: 45, // Insert
        addbrother: 13, // Enter
        editnode: 113,// F2
        delnode: 46, // Delete
        toggle: 32, // Space
        left: 37, // Left
        up: 38, // Up
        right: 39, // Right
        down: 40 // Down
      }
    }
  }

  // core object
  var jm = function(options) {
    jm.current = this

    this.version = __version__
    var opts = {}
    jm.util.json.merge(opts, DEFAULT_OPTIONS)
    jm.util.json.merge(opts, options)

    if (!opts.container) {
      logger.error('the options.container should not be null or empty.')
      return
    }
    this.options = opts
    this.inited = false
    this.mind = null
    this.event_handles = []
    this.init()
  }

  // ============= static object =============================================
  jm.direction = { left: -1, center: 0, right: 1 }
  jm.event_type = { show: 1, resize: 2, edit: 3, select: 4 }
  jm.key = { meta: 1 << 13, ctrl: 1 << 12, alt: 1 << 11, shift: 1 << 10 }

  jm.node = function(sId, iIndex, sTopic, oData, bIsRoot, oParent, eDirection, bExpanded) {
    if (!sId) {
      logger.error('invalid nodeid')
      return
    }
    if (typeof iIndex != 'number') {
      logger.error('invalid node index')
      return
    }
    if (typeof bExpanded === 'undefined') {
      bExpanded = true
    }
    this.id = sId
    this.index = iIndex
    this.topic = sTopic
    this.data = oData || {}
    this.isroot = bIsRoot
    this.parent = oParent
    this.direction = eDirection
    this.expanded = !!bExpanded
    this.children = []
    this._data = {}
  }

  jm.node.compare = function(node1, node2) {
    // '-1' is alwary the last
    var r = 0
    var i1 = node1.index
    var i2 = node2.index
    if (i1 >= 0 && i2 >= 0) {
      r = i1 - i2
    } else if (i1 == -1 && i2 == -1) {
      r = 0
    } else if (i1 == -1) {
      r = 1
    } else if (i2 == -1) {
      r = -1
    } else {
      r = 0
    }
    //logger.debug(i1+' <> '+i2+'  =  '+r);
    return r
  }

  jm.node.inherited = function(pnode, node) {
    if (!!pnode && !!node) {
      if (pnode.id === node.id) {
        return true
      }
      if (pnode.isroot) {
        return true
      }
      var pid = pnode.id
      var p = node
      while (!p.isroot) {
        p = p.parent
        if (p.id === pid) {
          return true
        }
      }
    }
    return false
  }

  jm.node.prototype = {
    get_location: function() {
      var vd = this._data.view
      return {
        x: vd.abs_x,
        y: vd.abs_y
      }
    },
    get_size: function() {
      var vd = this._data.view
      return {
        w: vd.width,
        h: vd.height
      }
    }
  }


  jm.mind = function() {
    this.name = null
    this.author = null
    this.version = null
    this.root = null
    this.selected = null
    this.nodes = {}
  }

  jm.mind.prototype = {
    get_node: function(nodeid) {
      if (nodeid in this.nodes) {
        return this.nodes[nodeid]
      } else {
        logger.warn('the node[id=' + nodeid + '] can not be found')
        return null
      }
    },

    set_root: function(nodeid, topic, data) {
      if (this.root == null) {
        this.root = new jm.node(nodeid, 0, topic, data, true)
        this._put_node(this.root)
      } else {
        logger.error('root node is already exist')
      }
    },

    add_node: function(parent_node, nodeid, topic, data, idx, direction, expanded) {
      if (!jm.util.is_node(parent_node)) {
        var the_parent_node = this.get_node(parent_node)
        if (!the_parent_node) {
          logger.error('the parent_node[id=' + parent_node + '] can not be found.')
          return null
        } else {
          return this.add_node(the_parent_node, nodeid, topic, data, idx, direction, expanded)
        }
      }
      var nodeindex = idx || -1
      var node = null
      if (parent_node.isroot) {
        var d = jm.direction.right
        if (isNaN(direction)) {
          var children = parent_node.children
          var children_len = children.length
          var r = 0
          for (var i = 0; i < children_len; i++) {
            if (children[i].direction === jm.direction.left) {
              r--
            } else {
              r++
            }
          }
          d = (children_len > 1 && r > 0) ? jm.direction.left : jm.direction.right
        } else {
          d = (direction != jm.direction.left) ? jm.direction.right : jm.direction.left
        }
        node = new jm.node(nodeid, nodeindex, topic, data, false, parent_node, d, expanded)
      } else {
        node = new jm.node(nodeid, nodeindex, topic, data, false, parent_node, parent_node.direction, expanded)
      }
      if (this._put_node(node)) {
        parent_node.children.push(node)
        this._reindex(parent_node)
      } else {
        logger.error('fail, the nodeid \'' + node.id + '\' has been already exist.')
        node = null
      }
      return node
    },

    insert_node_before: function(node_before, nodeid, topic, data) {
      if (!jm.util.is_node(node_before)) {
        var the_node_before = this.get_node(node_before)
        if (!the_node_before) {
          logger.error('the node_before[id=' + node_before + '] can not be found.')
          return null
        } else {
          return this.insert_node_before(the_node_before, nodeid, topic, data)
        }
      }
      var node_index = node_before.index - 0.5
      return this.add_node(node_before.parent, nodeid, topic, data, node_index)
    },

    get_node_before: function(node) {
      if (!jm.util.is_node(node)) {
        var the_node = this.get_node(node)
        if (!the_node) {
          logger.error('the node[id=' + node + '] can not be found.')
          return null
        } else {
          return this.get_node_before(the_node)
        }
      }
      if (node.isroot) {
        return null
      }
      var idx = node.index - 2
      if (idx >= 0) {
        return node.parent.children[idx]
      } else {
        return null
      }
    },

    insert_node_after: function(node_after, nodeid, topic, data) {
      if (!jm.util.is_node(node_after)) {
        var the_node_after = this.get_node(node_after)
        if (!the_node_after) {
          logger.error('the node_after[id=' + node_after + '] can not be found.')
          return null
        } else {
          return this.insert_node_after(the_node_after, nodeid, topic, data)
        }
      }
      var node_index = node_after.index + 0.5
      return this.add_node(node_after.parent, nodeid, topic, data, node_index)
    },

    get_node_after: function(node) {
      if (!jm.util.is_node(node)) {
        var the_node = this.get_node(node)
        if (!the_node) {
          logger.error('the node[id=' + node + '] can not be found.')
          return null
        } else {
          return this.get_node_after(the_node)
        }
      }
      if (node.isroot) {
        return null
      }
      var idx = node.index
      var brothers = node.parent.children
      if (brothers.length >= idx) {
        return node.parent.children[idx]
      } else {
        return null
      }
    },

    move_node: function(node, beforeid, parentid, direction) {
      if (!jm.util.is_node(node)) {
        var the_node = this.get_node(node)
        if (!the_node) {
          logger.error('the node[id=' + node + '] can not be found.')
          return null
        } else {
          return this.move_node(the_node, beforeid, parentid, direction)
        }
      }
      if (!parentid) {
        parentid = node.parent.id
      }
      return this._move_node(node, beforeid, parentid, direction)
    },

    _flow_node_direction: function(node, direction) {
      if (typeof direction === 'undefined') {
        direction = node.direction
      } else {
        node.direction = direction
      }
      var len = node.children.length
      while (len--) {
        this._flow_node_direction(node.children[len], direction)
      }
    },

    _move_node_internal: function(node, beforeid) {
      if (!!node && !!beforeid) {
        if (beforeid == '_last_') {
          node.index = -1
          this._reindex(node.parent)
        } else if (beforeid == '_first_') {
          node.index = 0
          this._reindex(node.parent)
        } else {
          var node_before = (!!beforeid) ? this.get_node(beforeid) : null
          if (node_before != null && node_before.parent != null && node_before.parent.id == node.parent.id) {
            node.index = node_before.index - 0.5
            this._reindex(node.parent)
          }
        }
      }
      return node
    },

    _move_node: function(node, beforeid, parentid, direction) {
      if (!!node && !!parentid) {
        if (node.parent.id != parentid) {
          // remove from parent's children
          var sibling = node.parent.children
          var si = sibling.length
          while (si--) {
            if (sibling[si].id == node.id) {
              sibling.splice(si, 1)
              break
            }
          }
          node.parent = this.get_node(parentid)
          node.parent.children.push(node)
        }

        if (node.parent.isroot) {
          if (direction == jm.direction.left) {
            node.direction = direction
          } else {
            node.direction = jm.direction.right
          }
        } else {
          node.direction = node.parent.direction
        }
        this._move_node_internal(node, beforeid)
        this._flow_node_direction(node)
      }
      return node
    },

    remove_node: function(node) {
      if (!jm.util.is_node(node)) {
        var the_node = this.get_node(node)
        if (!the_node) {
          logger.error('the node[id=' + node + '] can not be found.')
          return false
        } else {
          return this.remove_node(the_node)
        }
      }
      if (!node) {
        logger.error('fail, the node can not be found')
        return false
      }
      if (node.isroot) {
        logger.error('fail, can not remove root node')
        return false
      }
      if (this.selected != null && this.selected.id == node.id) {
        this.selected = null
      }
      // clean all subordinate nodes
      var children = node.children
      var ci = children.length
      while (ci--) {
        this.remove_node(children[ci])
      }
      // clean all children
      children.length = 0
      // remove from parent's children
      var sibling = node.parent.children
      var si = sibling.length
      while (si--) {
        if (sibling[si].id == node.id) {
          sibling.splice(si, 1)
          break
        }
      }
      // remove from global nodes
      delete this.nodes[node.id]
      // clean all properties
      for (var k in node) {
        delete node[k]
      }
      // remove it's self
      node = null
      //delete node;
      return true
    },

    _put_node: function(node) {
      if (node.id in this.nodes) {
        logger.warn('the nodeid \'' + node.id + '\' has been already exist.')
        return false
      } else {
        this.nodes[node.id] = node
        return true
      }
    },

    _reindex: function(node) {
      if (node instanceof jm.node) {
        node.children.sort(jm.node.compare)
        for (var i = 0; i < node.children.length; i++) {
          node.children[i].index = i + 1
        }
      }
    }
  }

  jm.format = {
    node_tree: {
      example: {
        'meta': {
          'name': __name__,
          'author': __author__,
          'version': __version__
        },
        'format': 'node_tree',
        'data': { 'id': 'root', 'topic': 'jsMind Example' }
      },
      get_mind: function(source) {
        var df = jm.format.node_tree
        var mind = new jm.mind()
        mind.name = source.meta.name
        mind.author = source.meta.author
        mind.version = source.meta.version
        df._parse(mind, source.data)
        return mind
      },
      get_data: function(mind) {
        var df = jm.format.node_tree
        var json = {}
        json.meta = {
          name: mind.name,
          author: mind.author,
          version: mind.version
        }
        json.format = 'node_tree'
        json.data = df._buildnode(mind.root)
        return json
      },

      _parse: function(mind, node_root) {
        var df = jm.format.node_tree
        var data = df._extract_data(node_root)
        mind.set_root(node_root.id, node_root.topic, data)
        if ('children' in node_root) {
          var children = node_root.children
          for (var i = 0; i < children.length; i++) {
            df._extract_subnode(mind, mind.root, children[i])
          }
        }
      },

      _extract_data: function(node_json) {
        var data = {}
        for (var k in node_json) {
          if (k == 'id' || k == 'topic' || k == 'children' || k == 'direction' || k == 'expanded') {
            continue
          }
          data[k] = node_json[k]
        }
        return data
      },

      _extract_subnode: function(mind, node_parent, node_json) {
        var df = jm.format.node_tree
        var data = df._extract_data(node_json)
        var d = null
        if (node_parent.isroot) {
          d = node_json.direction == 'left' ? jm.direction.left : jm.direction.right
        }
        var node = mind.add_node(node_parent, node_json.id, node_json.topic, data, null, d, node_json.expanded)
        if (!!node_json['children']) {
          var children = node_json.children
          for (var i = 0; i < children.length; i++) {
            df._extract_subnode(mind, node, children[i])
          }
        }
      },

      _buildnode: function(node) {
        var df = jm.format.node_tree
        if (!(node instanceof jm.node)) {
          return
        }
        var o = {
          id: node.id,
          topic: node.topic,
          expanded: node.expanded
        }
        if (!!node.parent && node.parent.isroot) {
          o.direction = node.direction == jm.direction.left ? 'left' : 'right'
        }
        if (node.data != null) {
          var node_data = node.data
          for (var k in node_data) {
            o[k] = node_data[k]
          }
        }
        var children = node.children
        if (children.length > 0) {
          o.children = []
          for (var i = 0; i < children.length; i++) {
            o.children.push(df._buildnode(children[i]))
          }
        }
        return o
      }
    },

    node_array: {
      example: {
        'meta': {
          'name': __name__,
          'author': __author__,
          'version': __version__
        },
        'format': 'node_array',
        'data': [
          { 'id': 'root', 'topic': 'jsMind Example', 'isroot': true }
        ]
      },

      get_mind: function(source) {
        var df = jm.format.node_array
        var mind = new jm.mind()
        mind.name = source.meta.name
        mind.author = source.meta.author
        mind.version = source.meta.version
        df._parse(mind, source.data)
        return mind
      },

      get_data: function(mind) {
        var df = jm.format.node_array
        var json = {}
        json.meta = {
          name: mind.name,
          author: mind.author,
          version: mind.version
        }
        json.format = 'node_array'
        json.data = []
        df._array(mind, json.data)
        return json
      },

      _parse: function(mind, node_array) {
        var df = jm.format.node_array
        var narray = node_array.slice(0)
        // reverse array for improving looping performance
        narray.reverse()
        var root_id = df._extract_root(mind, narray)
        if (!!root_id) {
          df._extract_subnode(mind, root_id, narray)
        } else {
          logger.error('root node can not be found')
        }
      },

      _extract_root: function(mind, node_array) {
        var df = jm.format.node_array
        var i = node_array.length
        while (i--) {
          if ('isroot' in node_array[i] && node_array[i].isroot) {
            var root_json = node_array[i]
            var data = df._extract_data(root_json)
            mind.set_root(root_json.id, root_json.topic, data)
            node_array.splice(i, 1)
            return root_json.id
          }
        }
        return null
      },

      _extract_subnode: function(mind, parentid, node_array) {
        var df = jm.format.node_array
        var i = node_array.length
        var node_json = null
        var data = null
        var extract_count = 0
        while (i--) {
          node_json = node_array[i]
          if (node_json.parentid == parentid) {
            data = df._extract_data(node_json)
            var d = null
            var node_direction = node_json.direction
            if (!!node_direction) {
              d = node_direction == 'left' ? jm.direction.left : jm.direction.right
            }
            mind.add_node(parentid, node_json.id, node_json.topic, data, null, d, node_json.expanded)
            node_array.splice(i, 1)
            extract_count++
            var sub_extract_count = df._extract_subnode(mind, node_json.id, node_array)
            if (sub_extract_count > 0) {
              // reset loop index after extract subordinate node
              i = node_array.length
              extract_count += sub_extract_count
            }
          }
        }
        return extract_count
      },

      _extract_data: function(node_json) {
        var data = {}
        for (var k in node_json) {
          if (k == 'id' || k == 'topic' || k == 'parentid' || k == 'isroot' || k == 'direction' || k == 'expanded') {
            continue
          }
          data[k] = node_json[k]
        }
        return data
      },

      _array: function(mind, node_array) {
        var df = jm.format.node_array
        df._array_node(mind.root, node_array)
      },

      _array_node: function(node, node_array) {
        var df = jm.format.node_array
        if (!(node instanceof jm.node)) {
          return
        }
        var o = {
          id: node.id,
          topic: node.topic,
          expanded: node.expanded
        }
        if (!!node.parent) {
          o.parentid = node.parent.id
        }
        if (node.isroot) {
          o.isroot = true
        }
        if (!!node.parent && node.parent.isroot) {
          o.direction = node.direction == jm.direction.left ? 'left' : 'right'
        }
        if (node.data != null) {
          var node_data = node.data
          for (var k in node_data) {
            o[k] = node_data[k]
          }
        }
        node_array.push(o)
        var ci = node.children.length
        for (var i = 0; i < ci; i++) {
          df._array_node(node.children[i], node_array)
        }
      }
    },

    freemind: {
      example: {
        'meta': {
          'name': __name__,
          'author': __author__,
          'version': __version__
        },
        'format': 'freemind',
        'data': '<map version="1.0.1"><node ID="root" TEXT="freemind Example"/></map>'
      },
      get_mind: function(source) {
        var df = jm.format.freemind
        var mind = new jm.mind()
        mind.name = source.meta.name
        mind.author = source.meta.author
        mind.version = source.meta.version
        var xml = source.data
        var xml_doc = df._parse_xml(xml)
        var xml_root = df._find_root(xml_doc)
        df._load_node(mind, null, xml_root)
        return mind
      },

      get_data: function(mind) {
        var df = jm.format.freemind
        var json = {}
        json.meta = {
          name: mind.name,
          author: mind.author,
          version: mind.version
        }
        json.format = 'freemind'
        var xmllines = []
        xmllines.push('<map version=\"1.0.1\">')
        df._buildmap(mind.root, xmllines)
        xmllines.push('</map>')
        json.data = xmllines.join(' ')
        return json
      },

      _parse_xml: function(xml) {
        var xml_doc = null
        if (window.DOMParser) {
          var parser = new DOMParser()
          xml_doc = parser.parseFromString(xml, 'text/xml')
        } else { // Internet Explorer
          xml_doc = new ActiveXObject('Microsoft.XMLDOM')
          xml_doc.async = false
          xml_doc.loadXML(xml)
        }
        return xml_doc
      },

      _find_root: function(xml_doc) {
        var nodes = xml_doc.childNodes
        var node = null
        var root = null
        var n = null
        for (var i = 0; i < nodes.length; i++) {
          n = nodes[i]
          if (n.nodeType == 1 && n.tagName == 'map') {
            node = n
            break
          }
        }
        if (!!node) {
          var ns = node.childNodes
          node = null
          for (var i = 0; i < ns.length; i++) {
            n = ns[i]
            if (n.nodeType == 1 && n.tagName == 'node') {
              node = n
              break
            }
          }
        }
        return node
      },

      _load_node: function(mind, parent_id, xml_node) {
        var df = jm.format.freemind
        var node_id = xml_node.getAttribute('ID')
        var node_topic = xml_node.getAttribute('TEXT')
        // look for richcontent
        if (node_topic == null) {
          var topic_children = xml_node.childNodes
          var topic_child = null
          for (var i = 0; i < topic_children.length; i++) {
            topic_child = topic_children[i]
            //logger.debug(topic_child.tagName);
            if (topic_child.nodeType == 1 && topic_child.tagName === 'richcontent') {
              node_topic = topic_child.textContent
              break
            }
          }
        }
        var node_data = df._load_attributes(xml_node)
        var node_expanded = ('expanded' in node_data) ? (node_data.expanded == 'true') : true
        delete node_data.expanded

        var node_position = xml_node.getAttribute('POSITION')
        var node_direction = null
        if (!!node_position) {
          node_direction = node_position == 'left' ? jm.direction.left : jm.direction.right
        }
        //logger.debug(node_position +':'+ node_direction);
        if (!!parent_id) {
          mind.add_node(parent_id, node_id, node_topic, node_data, null, node_direction, node_expanded)
        } else {
          mind.set_root(node_id, node_topic, node_data)
        }
        var children = xml_node.childNodes
        var child = null
        for (var i = 0; i < children.length; i++) {
          child = children[i]
          if (child.nodeType == 1 && child.tagName == 'node') {
            df._load_node(mind, node_id, child)
          }
        }
      },

      _load_attributes: function(xml_node) {
        var children = xml_node.childNodes
        var attr = null
        var attr_data = {}
        for (var i = 0; i < children.length; i++) {
          attr = children[i]
          if (attr.nodeType == 1 && attr.tagName === 'attribute') {
            attr_data[attr.getAttribute('NAME')] = attr.getAttribute('VALUE')
          }
        }
        return attr_data
      },

      _buildmap: function(node, xmllines) {
        var df = jm.format.freemind
        var pos = null
        if (!!node.parent && node.parent.isroot) {
          pos = node.direction === jm.direction.left ? 'left' : 'right'
        }
        xmllines.push('<node')
        xmllines.push('ID=\"' + node.id + '\"')
        if (!!pos) {
          xmllines.push('POSITION=\"' + pos + '\"')
        }
        xmllines.push('TEXT=\"' + node.topic + '\">')

        // store expanded status as an attribute
        xmllines.push('<attribute NAME=\"expanded\" VALUE=\"' + node.expanded + '\"/>')

        // for attributes
        var node_data = node.data
        if (node_data != null) {
          for (var k in node_data) {
            xmllines.push('<attribute NAME=\"' + k + '\" VALUE=\"' + node_data[k] + '\"/>')
          }
        }

        // for children
        var children = node.children
        for (var i = 0; i < children.length; i++) {
          df._buildmap(children[i], xmllines)
        }

        xmllines.push('</node>')
      }
    }
  }

  // ============= utility object =============================================

  jm.util = {
    is_node: function(node) {
      return !!node && node instanceof jm.node
    },
    ajax: {
      _xhr: function() {
        var xhr = null
        if (window.XMLHttpRequest) {
          xhr = new XMLHttpRequest()
        } else {
          try {
            xhr = new ActiveXObject('Microsoft.XMLHTTP')
          } catch (e) {
          }
        }
        return xhr
      },
      _eurl: function(url) {
        return encodeURIComponent(url)
      },
      request: function(url, param, method, callback, fail_callback) {
        var a = jm.util.ajax
        var p = null
        var tmp_param = []
        for (var k in param) {
          tmp_param.push(a._eurl(k) + '=' + a._eurl(param[k]))
        }
        if (tmp_param.length > 0) {
          p = tmp_param.join('&')
        }
        var xhr = a._xhr()
        if (!xhr) {
          return
        }
        xhr.onreadystatechange = function() {
          if (xhr.readyState == 4) {
            if (xhr.status == 200 || xhr.status == 0) {
              if (typeof callback === 'function') {
                var data = jm.util.json.string2json(xhr.responseText)
                if (data != null) {
                  callback(data)
                } else {
                  callback(xhr.responseText)
                }
              }
            } else {
              if (typeof fail_callback === 'function') {
                fail_callback(xhr)
              } else {
                logger.error('xhr request failed.', xhr)
              }
            }
          }
        }
        method = method || 'GET'
        xhr.open(method, url, true)
        xhr.setRequestHeader('If-Modified-Since', '0')
        if (method == 'POST') {
          xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=utf-8')
          xhr.send(p)
        } else {
          xhr.send()
        }
      },
      get: function(url, callback) {
        return jm.util.ajax.request(url, {}, 'GET', callback)
      },
      post: function(url, param, callback) {
        return jm.util.ajax.request(url, param, 'POST', callback)
      }
    },

    dom: {
      //target,eventType,handler
      add_event: function(t, e, h) {
        if (!!t.addEventListener) {
          t.addEventListener(e, h, false)
        } else {
          t.attachEvent('on' + e, h)
        }
      }
    },

    file: {
      read: function(file_data, fn_callback) {
        var reader = new FileReader()
        reader.onload = function() {
          if (typeof fn_callback === 'function') {
            fn_callback(this.result, file_data.name)
          }
        }
        reader.readAsText(file_data)
      },

      save: function(file_data, type, name) {
        var blob
        if (typeof $w.Blob === 'function') {
          blob = new Blob([file_data], { type: type })
        } else {
          var BlobBuilder = $w.BlobBuilder || $w.MozBlobBuilder || $w.WebKitBlobBuilder || $w.MSBlobBuilder
          var bb = new BlobBuilder()
          bb.append(file_data)
          blob = bb.getBlob(type)
        }
        if (navigator.msSaveBlob) {
          navigator.msSaveBlob(blob, name)
        } else {
          var URL = $w.URL || $w.webkitURL
          var bloburl = URL.createObjectURL(blob)
          var anchor = $c('a')
          if ('download' in anchor) {
            anchor.style.visibility = 'hidden'
            anchor.href = bloburl
            anchor.download = name
            $d.body.appendChild(anchor)
            var evt = $d.createEvent('MouseEvents')
            evt.initEvent('click', true, true)
            anchor.dispatchEvent(evt)
            $d.body.removeChild(anchor)
          } else {
            location.href = bloburl
          }
        }
      }
    },

    json: {
      json2string: function(json) {
        if (!!JSON) {
          try {
            var json_str = JSON.stringify(json)
            return json_str
          } catch (e) {
            logger.warn(e)
            logger.warn('can not convert to string')
            return null
          }
        }
      },
      string2json: function(json_str) {
        if (!!JSON) {
          try {
            var json = JSON.parse(json_str)
            return json
          } catch (e) {
            logger.warn(e)
            logger.warn('can not parse to json')
            return null
          }
        }
      },
      merge: function(b, a) {
        for (var o in a) {
          if (o in b) {
            if (typeof b[o] === 'object' &&
              Object.prototype.toString.call(b[o]).toLowerCase() == '[object object]' &&
              !b[o].length) {
              jm.util.json.merge(b[o], a[o])
            } else {
              b[o] = a[o]
            }
          } else {
            b[o] = a[o]
          }
        }
        return b
      }
    },

    uuid: {
      newid: function() {
        return (new Date().getTime().toString(16) + Math.random().toString(16).substr(2)).substr(2, 16)
      }
    },

    text: {
      is_empty: function(s) {
        if (!s) {
          return true
        }
        return s.replace(/\s*/, '').length == 0
      }
    }
  }

  jm.prototype = {
    init: function() {
      if (this.inited) {
        return
      }
      this.inited = true

      var opts = this.options

      var opts_layout = {
        mode: opts.mode,
        hspace: opts.layout.hspace,
        vspace: opts.layout.vspace,
        pspace: opts.layout.pspace
      }
      var opts_view = {
        container: opts.container,
        support_html: opts.support_html,
        engine: opts.view.engine,
        hmargin: opts.view.hmargin,
        vmargin: opts.view.vmargin,
        line_width: opts.view.line_width,
        line_color: opts.view.line_color
      }
      // create instance of function provider
      this.data = new jm.data_provider(this)
      this.layout = new jm.layout_provider(this, opts_layout)
      this.view = new jm.view_provider(this, opts_view)
      this.shortcut = new jm.shortcut_provider(this, opts.shortcut)

      this.data.init()
      this.layout.init()
      this.view.init()
      this.shortcut.init()

      this._event_bind()

      jm.init_plugins(this)
    },

    enable_edit: function() {
      this.options.editable = true
    },

    disable_edit: function() {
      this.options.editable = false
    },

    // call enable_event_handle('dblclick')
    // options are 'mousedown', 'click', 'dblclick'
    enable_event_handle: function(event_handle) {
      this.options.default_event_handle['enable_' + event_handle + '_handle'] = true
    },

    // call disable_event_handle('dblclick')
    // options are 'mousedown', 'click', 'dblclick'
    disable_event_handle: function(event_handle) {
      this.options.default_event_handle['enable_' + event_handle + '_handle'] = false
    },

    get_editable: function() {
      return this.options.editable
    },

    set_theme: function(theme) {
      var theme_old = this.options.theme
      this.options.theme = (!!theme) ? theme : null
      if (theme_old != this.options.theme) {
        this.view.reset_theme()
        this.view.reset_custom_style()
      }
    },
    _event_bind: function() {
      this.view.add_event(this, 'mousedown', this.mousedown_handle)
      this.view.add_event(this, 'click', this.click_handle)
      this.view.add_event(this, 'dblclick', this.dblclick_handle)
    },

    mousedown_handle: function(e) {
      if (!this.options.default_event_handle['enable_mousedown_handle']) {
        return
      }
      var element = e.target || event.srcElement
      var nodeid = this.view.get_binded_nodeid(element)
      if (!!nodeid) {
        if (element.tagName.toLowerCase() == 'jmnode') {
          this.select_node(nodeid)
        }
      } else {
        this.select_clear()
      }
    },

    click_handle: function(e) {
      if (!this.options.default_event_handle['enable_click_handle']) {
        return
      }
      var element = e.target || event.srcElement
      var isexpander = this.view.is_expander(element)
      if (isexpander) {
        var nodeid = this.view.get_binded_nodeid(element)
        if (!!nodeid) {
          this.toggle_node(nodeid)
        }
      }
    },

    dblclick_handle: function(e) {
      if (!this.options.default_event_handle['enable_dblclick_handle']) {
        return
      }
      if (this.get_editable()) {
        var element = e.target || event.srcElement
        var nodeid = this.view.get_binded_nodeid(element)
        if (!!nodeid) {
          this.begin_edit(nodeid)
        }
      }
    },

    begin_edit: function(node) {
      if (!jm.util.is_node(node)) {
        var the_node = this.get_node(node)
        if (!the_node) {
          logger.error('the node[id=' + node + '] can not be found.')
          return false
        } else {
          return this.begin_edit(the_node)
        }
      }
      if (this.get_editable()) {
        this.view.edit_node_begin(node)
      } else {
        logger.error('fail, this mind map is not editable.')
        return
      }
    },

    end_edit: function() {
      this.view.edit_node_end()
    },

    toggle_node: function(node) {
      if (!jm.util.is_node(node)) {
        var the_node = this.get_node(node)
        if (!the_node) {
          logger.error('the node[id=' + node + '] can not be found.')
          return
        } else {
          return this.toggle_node(the_node)
        }
      }
      if (node.isroot) {
        return
      }
      this.view.save_location(node)
      this.layout.toggle_node(node)
      this.view.relayout()
      this.view.restore_location(node)
    },

    expand_node: function(node) {
      if (!jm.util.is_node(node)) {
        var the_node = this.get_node(node)
        if (!the_node) {
          logger.error('the node[id=' + node + '] can not be found.')
          return
        } else {
          return this.expand_node(the_node)
        }
      }
      if (node.isroot) {
        return
      }
      this.view.save_location(node)
      this.layout.expand_node(node)
      this.view.relayout()
      this.view.restore_location(node)
    },

    collapse_node: function(node) {
      if (!jm.util.is_node(node)) {
        var the_node = this.get_node(node)
        if (!the_node) {
          logger.error('the node[id=' + node + '] can not be found.')
          return
        } else {
          return this.collapse_node(the_node)
        }
      }
      if (node.isroot) {
        return
      }
      this.view.save_location(node)
      this.layout.collapse_node(node)
      this.view.relayout()
      this.view.restore_location(node)
    },

    expand_all: function() {
      this.layout.expand_all()
      this.view.relayout()
    },

    collapse_all: function() {
      this.layout.collapse_all()
      this.view.relayout()
    },

    expand_to_depth: function(depth) {
      this.layout.expand_to_depth(depth)
      this.view.relayout()
    },

    _reset: function() {
      this.view.reset()
      this.layout.reset()
      this.data.reset()
    },

    _show: function(mind) {
      var m = mind || jm.format.node_array.example

      this.mind = this.data.load(m)
      if (!this.mind) {
        logger.error('data.load error')
        return
      } else {
        logger.debug('data.load ok')
      }

      this.view.load()
      logger.debug('view.load ok')

      this.layout.layout()
      logger.debug('layout.layout ok')

      this.view.show(true)
      logger.debug('view.show ok')

      this.invoke_event_handle(jm.event_type.show, { data: [mind] })
    },

    show: function(mind) {
      this._reset()
      this._show(mind)
    },

    get_meta: function() {
      return {
        name: this.mind.name,
        author: this.mind.author,
        version: this.mind.version
      }
    },

    get_data: function(data_format) {
      var df = data_format || 'node_tree'
      return this.data.get_data(df)
    },

    get_root: function() {
      return this.mind.root
    },

    get_node: function(nodeid) {
      return this.mind.get_node(nodeid)
    },

    add_node: function(parent_node, nodeid, topic, data) {
      if (this.get_editable()) {
        var node = this.mind.add_node(parent_node, nodeid, topic, data)
        if (!!node) {
          this.view.add_node(node)
          this.layout.layout()
          this.view.show(false)
          this.view.reset_node_custom_style(node)
          this.expand_node(parent_node)
          this.invoke_event_handle(jm.event_type.edit, {
            evt: 'add_node',
            data: [parent_node.id, nodeid, topic, data],
            node: nodeid
          })
        }
        return node
      } else {
        logger.error('fail, this mind map is not editable')
        return null
      }
    },

    insert_node_before: function(node_before, nodeid, topic, data) {
      if (this.get_editable()) {
        var beforeid = jm.util.is_node(node_before) ? node_before.id : node_before
        var node = this.mind.insert_node_before(node_before, nodeid, topic, data)
        if (!!node) {
          this.view.add_node(node)
          this.layout.layout()
          this.view.show(false)
          this.invoke_event_handle(jm.event_type.edit, {
            evt: 'insert_node_before',
            data: [beforeid, nodeid, topic, data],
            node: nodeid
          })
        }
        return node
      } else {
        logger.error('fail, this mind map is not editable')
        return null
      }
    },

    insert_node_after: function(node_after, nodeid, topic, data) {
      if (this.get_editable()) {
        var afterid = jm.util.is_node(node_after) ? node_after.id : node_after
        var node = this.mind.insert_node_after(node_after, nodeid, topic, data)
        if (!!node) {
          this.view.add_node(node)
          this.layout.layout()
          this.view.show(false)
          this.invoke_event_handle(jm.event_type.edit, {
            evt: 'insert_node_after',
            data: [afterid, nodeid, topic, data],
            node: nodeid
          })
        }
        return node
      } else {
        logger.error('fail, this mind map is not editable')
        return null
      }
    },

    remove_node: function(node) {
      if (!jm.util.is_node(node)) {
        var the_node = this.get_node(node)
        if (!the_node) {
          logger.error('the node[id=' + node + '] can not be found.')
          return false
        } else {
          return this.remove_node(the_node)
        }
      }
      if (this.get_editable()) {
        if (node.isroot) {
          logger.error('fail, can not remove root node')
          return false
        }
        var nodeid = node.id
        var parentid = node.parent.id
        var parent_node = this.get_node(parentid)
        this.view.save_location(parent_node)
        this.view.remove_node(node)
        this.mind.remove_node(node)
        this.layout.layout()
        this.view.show(false)
        this.view.restore_location(parent_node)
        this.invoke_event_handle(jm.event_type.edit, { evt: 'remove_node', data: [nodeid], node: parentid })
        return true
      } else {
        logger.error('fail, this mind map is not editable')
        return false
      }
    },

    update_node: function(nodeid, topic) {
      if (this.get_editable()) {
        if (jm.util.text.is_empty(topic)) {
          logger.warn('fail, topic can not be empty')
          return
        }
        var node = this.get_node(nodeid)
        if (!!node) {
          if (node.topic === topic) {
            logger.info('nothing changed')
            this.view.update_node(node)
            return
          }
          node.topic = topic
          this.view.update_node(node)
          this.layout.layout()
          this.view.show(false)
          this.invoke_event_handle(jm.event_type.edit, { evt: 'update_node', data: [nodeid, topic], node: nodeid })
        }
      } else {
        logger.error('fail, this mind map is not editable')
        return
      }
    },

    move_node: function(nodeid, beforeid, parentid, direction) {
      if (this.get_editable()) {
        var node = this.mind.move_node(nodeid, beforeid, parentid, direction)
        if (!!node) {
          this.view.update_node(node)
          this.layout.layout()
          this.view.show(false)
          this.invoke_event_handle(jm.event_type.edit, {
            evt: 'move_node',
            data: [nodeid, beforeid, parentid, direction],
            node: nodeid
          })
        }
      } else {
        logger.error('fail, this mind map is not editable')
        return
      }
    },

    select_node: function(node) {
      if (!jm.util.is_node(node)) {
        var the_node = this.get_node(node)
        if (!the_node) {
          logger.error('the node[id=' + node + '] can not be found.')
          return
        } else {
          return this.select_node(the_node)
        }
      }
      if (!this.layout.is_visible(node)) {
        return
      }
      this.mind.selected = node
      this.view.select_node(node)
      this.invoke_event_handle(jm.event_type.select, { evt: 'select_node', data: [], node: node.id })
    },

    get_selected_node: function() {
      if (!!this.mind) {
        return this.mind.selected
      } else {
        return null
      }
    },

    select_clear: function() {
      if (!!this.mind) {
        this.mind.selected = null
        this.view.select_clear()
      }
    },

    is_node_visible: function(node) {
      return this.layout.is_visible(node)
    },

    find_node_before: function(node) {
      if (!jm.util.is_node(node)) {
        var the_node = this.get_node(node)
        if (!the_node) {
          logger.error('the node[id=' + node + '] can not be found.')
          return
        } else {
          return this.find_node_before(the_node)
        }
      }
      if (node.isroot) {
        return null
      }
      var n = null
      if (node.parent.isroot) {
        var c = node.parent.children
        var prev = null
        var ni = null
        for (var i = 0; i < c.length; i++) {
          ni = c[i]
          if (node.direction === ni.direction) {
            if (node.id === ni.id) {
              n = prev
            }
            prev = ni
          }
        }
      } else {
        n = this.mind.get_node_before(node)
      }
      return n
    },

    find_node_after: function(node) {
      if (!jm.util.is_node(node)) {
        var the_node = this.get_node(node)
        if (!the_node) {
          logger.error('the node[id=' + node + '] can not be found.')
          return
        } else {
          return this.find_node_after(the_node)
        }
      }
      if (node.isroot) {
        return null
      }
      var n = null
      if (node.parent.isroot) {
        var c = node.parent.children
        var getthis = false
        var ni = null
        for (var i = 0; i < c.length; i++) {
          ni = c[i]
          if (node.direction === ni.direction) {
            if (getthis) {
              n = ni
              break
            }
            if (node.id === ni.id) {
              getthis = true
            }
          }
        }
      } else {
        n = this.mind.get_node_after(node)
      }
      return n
    },

    set_node_color: function(nodeid, bgcolor, fgcolor) {
      if (this.get_editable()) {
        var node = this.mind.get_node(nodeid)
        if (!!node) {
          if (!!bgcolor) {
            node.data['background-color'] = bgcolor
          }
          if (!!fgcolor) {
            node.data['foreground-color'] = fgcolor
          }
          this.view.reset_node_custom_style(node)
        }
      } else {
        logger.error('fail, this mind map is not editable')
        return null
      }
    },

    set_node_font_style: function(nodeid, size, weight, style) {
      if (this.get_editable()) {
        var node = this.mind.get_node(nodeid)
        if (!!node) {
          if (!!size) {
            node.data['font-size'] = size
          }
          if (!!weight) {
            node.data['font-weight'] = weight
          }
          if (!!style) {
            node.data['font-style'] = style
          }
          this.view.reset_node_custom_style(node)
          this.view.update_node(node)
          this.layout.layout()
          this.view.show(false)
        }
      } else {
        logger.error('fail, this mind map is not editable')
        return null
      }
    },

    set_node_background_image: function(nodeid, image, width, height, rotation) {
      if (this.get_editable()) {
        var node = this.mind.get_node(nodeid)
        if (!!node) {
          if (!!image) {
            node.data['background-image'] = image
          }
          if (!!width) {
            node.data['width'] = width
          }
          if (!!height) {
            node.data['height'] = height
          }
          if (!!rotation) {
            node.data['background-rotation'] = rotation
          }
          this.view.reset_node_custom_style(node)
          this.view.update_node(node)
          this.layout.layout()
          this.view.show(false)
        }
      } else {
        logger.error('fail, this mind map is not editable')
        return null
      }
    },

    set_node_background_rotation: function(nodeid, rotation) {
      if (this.get_editable()) {
        var node = this.mind.get_node(nodeid)
        if (!!node) {
          if (!node.data['background-image']) {
            logger.error('fail, only can change rotation angle of node with background image')
            return null
          }
          node.data['background-rotation'] = rotation
          this.view.reset_node_custom_style(node)
          this.view.update_node(node)
          this.layout.layout()
          this.view.show(false)
        }
      } else {
        logger.error('fail, this mind map is not editable')
        return null
      }
    },

    resize: function() {
      this.view.resize()
    },

    // callback(type ,data)
    add_event_listener: function(callback) {
      if (typeof callback === 'function') {
        this.event_handles.push(callback)
      }
    },

    clear_event_listener: function() {
      this.event_handles = []
    },

    invoke_event_handle: function(type, data) {
      var j = this
      $w.setTimeout(function() {
        j._invoke_event_handle(type, data)
      }, 0)
    },

    _invoke_event_handle: function(type, data) {
      var l = this.event_handles.length
      for (var i = 0; i < l; i++) {
        this.event_handles[i](type, data)
      }
    }

  }

  // ============= data provider =============================================

  jm.data_provider = function(jm) {
    this.jm = jm
  }

  jm.data_provider.prototype = {
    init: function() {
      logger.debug('data.init')
    },

    reset: function() {
      logger.debug('data.reset')
    },

    load: function(mind_data) {
      var df = null
      var mind = null
      if (typeof mind_data === 'object') {
        if (!!mind_data.format) {
          df = mind_data.format
        } else {
          df = 'node_tree'
        }
      } else {
        df = 'freemind'
      }

      if (df == 'node_array') {
        mind = jm.format.node_array.get_mind(mind_data)
      } else if (df == 'node_tree') {
        mind = jm.format.node_tree.get_mind(mind_data)
      } else if (df == 'freemind') {
        mind = jm.format.freemind.get_mind(mind_data)
      } else {
        logger.warn('unsupported format')
      }
      return mind
    },

    get_data: function(data_format) {
      var data = null
      if (data_format == 'node_array') {
        data = jm.format.node_array.get_data(this.jm.mind)
      } else if (data_format == 'node_tree') {
        data = jm.format.node_tree.get_data(this.jm.mind)
      } else if (data_format == 'freemind') {
        data = jm.format.freemind.get_data(this.jm.mind)
      } else {
        logger.error('unsupported ' + data_format + ' format')
      }
      return data
    }
  }

  // ============= layout provider ===========================================

  jm.layout_provider = function(jm, options) {
    this.opts = options
    this.jm = jm
    this.isside = (this.opts.mode == 'side')
    this.bounds = null

    this.cache_valid = false
  }

  jm.layout_provider.prototype = {
    init: function() {
      logger.debug('layout.init')
    },
    reset: function() {
      logger.debug('layout.reset')
      this.bounds = { n: 0, s: 0, w: 0, e: 0 }
    },
    layout: function() {
      logger.debug('layout.layout')
      this.layout_direction()
      this.layout_offset()
    },

    layout_direction: function() {
      this._layout_direction_root()
    },

    _layout_direction_root: function() {
      var node = this.jm.mind.root
      // logger.debug(node);
      var layout_data = null
      if ('layout' in node._data) {
        layout_data = node._data.layout
      } else {
        layout_data = {}
        node._data.layout = layout_data
      }
      var children = node.children
      var children_count = children.length
      layout_data.direction = jm.direction.center
      layout_data.side_index = 0
      if (this.isside) {
        var i = children_count
        while (i--) {
          this._layout_direction_side(children[i], jm.direction.right, i)
        }
      } else {
        var i = children_count
        var subnode = null
        while (i--) {
          subnode = children[i]
          if (subnode.direction == jm.direction.left) {
            this._layout_direction_side(subnode, jm.direction.left, i)
          } else {
            this._layout_direction_side(subnode, jm.direction.right, i)
          }
        }
        /*
                var boundary = Math.ceil(children_count/2);
                var i = children_count;
                while(i--){
                    if(i>=boundary){
                        this._layout_direction_side(children[i],jm.direction.left, children_count-i-1);
                    }else{
                        this._layout_direction_side(children[i],jm.direction.right, i);
                    }
                }*/

      }
    },

    _layout_direction_side: function(node, direction, side_index) {
      var layout_data = null
      if ('layout' in node._data) {
        layout_data = node._data.layout
      } else {
        layout_data = {}
        node._data.layout = layout_data
      }
      var children = node.children
      var children_count = children.length

      layout_data.direction = direction
      layout_data.side_index = side_index
      var i = children_count
      while (i--) {
        this._layout_direction_side(children[i], direction, i)
      }
    },

    layout_offset: function() {
      var node = this.jm.mind.root
      var layout_data = node._data.layout
      layout_data.offset_x = 0
      layout_data.offset_y = 0
      layout_data.outer_height = 0
      var children = node.children
      var i = children.length
      var left_nodes = []
      var right_nodes = []
      var subnode = null
      while (i--) {
        subnode = children[i]
        if (subnode._data.layout.direction == jm.direction.right) {
          right_nodes.unshift(subnode)
        } else {
          left_nodes.unshift(subnode)
        }
      }
      layout_data.left_nodes = left_nodes
      layout_data.right_nodes = right_nodes
      layout_data.outer_height_left = this._layout_offset_subnodes(left_nodes)
      layout_data.outer_height_right = this._layout_offset_subnodes(right_nodes)
      this.bounds.e = node._data.view.width / 2
      this.bounds.w = 0 - this.bounds.e
      //logger.debug(this.bounds.w);
      this.bounds.n = 0
      this.bounds.s = Math.max(layout_data.outer_height_left, layout_data.outer_height_right)
    },

    // layout both the x and y axis
    _layout_offset_subnodes: function(nodes) {
      var total_height = 0
      var nodes_count = nodes.length
      var i = nodes_count
      var node = null
      var node_outer_height = 0
      var layout_data = null
      var base_y = 0
      var pd = null // parent._data
      while (i--) {
        node = nodes[i]
        layout_data = node._data.layout
        if (pd == null) {
          pd = node.parent._data
        }

        node_outer_height = this._layout_offset_subnodes(node.children)
        if (!node.expanded) {
          node_outer_height = 0
          this.set_visible(node.children, false)
        }
        node_outer_height = Math.max(node._data.view.height, node_outer_height)

        layout_data.outer_height = node_outer_height
        layout_data.offset_y = base_y - node_outer_height / 2
        layout_data.offset_x = this.opts.hspace * layout_data.direction + pd.view.width * (pd.layout.direction + layout_data.direction) / 2
        if (!node.parent.isroot) {
          layout_data.offset_x += this.opts.pspace * layout_data.direction
        }

        base_y = base_y - node_outer_height - this.opts.vspace
        total_height += node_outer_height
      }
      if (nodes_count > 1) {
        total_height += this.opts.vspace * (nodes_count - 1)
      }
      i = nodes_count
      var middle_height = total_height / 2
      while (i--) {
        node = nodes[i]
        node._data.layout.offset_y += middle_height
      }
      return total_height
    },

    // layout the y axis only, for collapse/expand a node
    _layout_offset_subnodes_height: function(nodes) {
      var total_height = 0
      var nodes_count = nodes.length
      var i = nodes_count
      var node = null
      var node_outer_height = 0
      var layout_data = null
      var base_y = 0
      var pd = null // parent._data
      while (i--) {
        node = nodes[i]
        layout_data = node._data.layout
        if (pd == null) {
          pd = node.parent._data
        }

        node_outer_height = this._layout_offset_subnodes_height(node.children)
        if (!node.expanded) {
          node_outer_height = 0
        }
        node_outer_height = Math.max(node._data.view.height, node_outer_height)

        layout_data.outer_height = node_outer_height
        layout_data.offset_y = base_y - node_outer_height / 2
        base_y = base_y - node_outer_height - this.opts.vspace
        total_height += node_outer_height
      }
      if (nodes_count > 1) {
        total_height += this.opts.vspace * (nodes_count - 1)
      }
      i = nodes_count
      var middle_height = total_height / 2
      while (i--) {
        node = nodes[i]
        node._data.layout.offset_y += middle_height
        //logger.debug(node.topic);
        //logger.debug(node._data.layout.offset_y);
      }
      return total_height
    },

    get_node_offset: function(node) {
      var layout_data = node._data.layout
      var offset_cache = null
      if (('_offset_' in layout_data) && this.cache_valid) {
        offset_cache = layout_data._offset_
      } else {
        offset_cache = { x: -1, y: -1 }
        layout_data._offset_ = offset_cache
      }
      if (offset_cache.x == -1 || offset_cache.y == -1) {
        var x = layout_data.offset_x
        var y = layout_data.offset_y
        if (!node.isroot) {
          var offset_p = this.get_node_offset(node.parent)
          x += offset_p.x
          y += offset_p.y
        }
        offset_cache.x = x
        offset_cache.y = y
      }
      return offset_cache
    },

    get_node_point: function(node) {
      var view_data = node._data.view
      var offset_p = this.get_node_offset(node)
      //logger.debug(offset_p);
      var p = {}
      p.x = offset_p.x + view_data.width * (node._data.layout.direction - 1) / 2
      p.y = offset_p.y - view_data.height / 2
      //logger.debug(p);
      return p
    },

    get_node_point_in: function(node) {
      var p = this.get_node_offset(node)
      return p
    },

    get_node_point_out: function(node) {
      var layout_data = node._data.layout
      var pout_cache = null
      if (('_pout_' in layout_data) && this.cache_valid) {
        pout_cache = layout_data._pout_
      } else {
        pout_cache = { x: -1, y: -1 }
        layout_data._pout_ = pout_cache
      }
      if (pout_cache.x == -1 || pout_cache.y == -1) {
        if (node.isroot) {
          pout_cache.x = 0
          pout_cache.y = 0
        } else {
          var view_data = node._data.view
          var offset_p = this.get_node_offset(node)
          pout_cache.x = offset_p.x + (view_data.width + this.opts.pspace) * node._data.layout.direction
          pout_cache.y = offset_p.y
          //logger.debug('pout');
          //logger.debug(pout_cache);
        }
      }
      return pout_cache
    },

    get_expander_point: function(node) {
      var p = this.get_node_point_out(node)
      var ex_p = {}
      if (node._data.layout.direction == jm.direction.right) {
        ex_p.x = p.x - this.opts.pspace
      } else {
        ex_p.x = p.x
      }
      ex_p.y = p.y - Math.ceil(this.opts.pspace / 2)
      return ex_p
    },

    get_min_size: function() {
      var nodes = this.jm.mind.nodes
      var node = null
      var pout = null
      for (var nodeid in nodes) {
        node = nodes[nodeid]
        pout = this.get_node_point_out(node)
        if (pout.x > this.bounds.e) {
          this.bounds.e = pout.x
        }
        if (pout.x < this.bounds.w) {
          this.bounds.w = pout.x
        }
      }
      return {
        w: this.bounds.e - this.bounds.w,
        h: this.bounds.s - this.bounds.n
      }
    },

    toggle_node: function(node) {
      if (node.isroot) {
        return
      }
      if (node.expanded) {
        this.collapse_node(node)
      } else {
        this.expand_node(node)
      }
    },

    expand_node: function(node) {
      node.expanded = true
      this.part_layout(node)
      this.set_visible(node.children, true)
      this.jm.invoke_event_handle(jm.event_type.show, { evt: 'expand_node', data: [], node: node.id })
    },

    collapse_node: function(node) {
      node.expanded = false
      this.part_layout(node)
      this.set_visible(node.children, false)
      this.jm.invoke_event_handle(jm.event_type.show, { evt: 'collapse_node', data: [], node: node.id })
    },

    expand_all: function() {
      var nodes = this.jm.mind.nodes
      var c = 0
      var node
      for (var nodeid in nodes) {
        node = nodes[nodeid]
        if (!node.expanded) {
          node.expanded = true
          c++
        }
      }
      if (c > 0) {
        var root = this.jm.mind.root
        this.part_layout(root)
        this.set_visible(root.children, true)
      }
    },

    collapse_all: function() {
      var nodes = this.jm.mind.nodes
      var c = 0
      var node
      for (var nodeid in nodes) {
        node = nodes[nodeid]
        if (node.expanded && !node.isroot) {
          node.expanded = false
          c++
        }
      }
      if (c > 0) {
        var root = this.jm.mind.root
        this.part_layout(root)
        this.set_visible(root.children, true)
      }
    },

    expand_to_depth: function(target_depth, curr_nodes, curr_depth) {
      if (target_depth < 1) {
        return
      }
      var nodes = curr_nodes || this.jm.mind.root.children
      var depth = curr_depth || 1
      var i = nodes.length
      var node = null
      while (i--) {
        node = nodes[i]
        if (depth < target_depth) {
          if (!node.expanded) {
            this.expand_node(node)
          }
          this.expand_to_depth(target_depth, node.children, depth + 1)
        }
        if (depth == target_depth) {
          if (node.expanded) {
            this.collapse_node(node)
          }
        }
      }
    },

    part_layout: function(node) {
      var root = this.jm.mind.root
      if (!!root) {
        var root_layout_data = root._data.layout
        if (node.isroot) {
          root_layout_data.outer_height_right = this._layout_offset_subnodes_height(root_layout_data.right_nodes)
          root_layout_data.outer_height_left = this._layout_offset_subnodes_height(root_layout_data.left_nodes)
        } else {
          if (node._data.layout.direction == jm.direction.right) {
            root_layout_data.outer_height_right = this._layout_offset_subnodes_height(root_layout_data.right_nodes)
          } else {
            root_layout_data.outer_height_left = this._layout_offset_subnodes_height(root_layout_data.left_nodes)
          }
        }
        this.bounds.s = Math.max(root_layout_data.outer_height_left, root_layout_data.outer_height_right)
        this.cache_valid = false
      } else {
        logger.warn('can not found root node')
      }
    },

    set_visible: function(nodes, visible) {
      var i = nodes.length
      var node = null
      var layout_data = null
      while (i--) {
        node = nodes[i]
        layout_data = node._data.layout
        if (node.expanded) {
          this.set_visible(node.children, visible)
        } else {
          this.set_visible(node.children, false)
        }
        if (!node.isroot) {
          node._data.layout.visible = visible
        }
      }
    },

    is_expand: function(node) {
      return node.expanded
    },

    is_visible: function(node) {
      var layout_data = node._data.layout
      if (('visible' in layout_data) && !layout_data.visible) {
        return false
      } else {
        return true
      }
    }
  }

  jm.graph_canvas = function(view) {
    this.opts = view.opts
    this.e_canvas = $c('canvas')
    this.e_canvas.className = 'jsmind'
    this.canvas_ctx = this.e_canvas.getContext('2d')
    this.size = { w: 0, h: 0 }
  }

  jm.graph_canvas.prototype = {
    element: function() {
      return this.e_canvas
    },

    set_size: function(w, h) {
      this.size.w = w
      this.size.h = h
      this.e_canvas.width = w
      this.e_canvas.height = h
    },

    clear: function() {
      this.canvas_ctx.clearRect(0, 0, this.size.w, this.size.h)
    },

    draw_line: function(pout, pin, offset) {
      var ctx = this.canvas_ctx
      ctx.strokeStyle = this.opts.line_color
      ctx.lineWidth = this.opts.line_width
      ctx.lineCap = 'round'

      this._line_to(ctx,
        pin.x + offset.x,
        pin.y + offset.y,
        pout.x + offset.x,
        pout.y + offset.y)
    },

    copy_to: function(dest_canvas_ctx, callback) {
      dest_canvas_ctx.drawImage(this.e_canvas, 0, 0)
      !!callback && callback()
    },

    _bezier_to: function(ctx, x1, y1, x2, y2) {
      ctx.beginPath()
      ctx.moveTo(x1, y1)
      ctx.bezierCurveTo(x1 + (x2 - x1) * 2 / 3, y1, x1, y2, x2, y2)
      ctx.stroke()
    },

    _line_to: function(ctx, x1, y1, x2, y2) {
      ctx.beginPath()
      ctx.moveTo(x1, y1)
      ctx.lineTo(x2, y1)
      ctx.lineTo(x2, y2)
      ctx.stroke()
    }
  }

  jm.graph_svg = function(view) {
    this.view = view
    this.opts = view.opts
    this.e_svg = jm.graph_svg.c('svg')
    this.e_svg.setAttribute('class', 'jsmind')
    this.size = { w: 0, h: 0 }
    this.lines = []
  }

  jm.graph_svg.c = function(tag) {
    return $d.createElementNS('http://www.w3.org/2000/svg', tag)
  }

  jm.graph_svg.prototype = {
    element: function() {
      return this.e_svg
    },

    set_size: function(w, h) {
      this.size.w = w
      this.size.h = h
      this.e_svg.setAttribute('width', w)
      this.e_svg.setAttribute('height', h)
    },

    clear: function() {
      var len = this.lines.length
      while (len--) {
        this.e_svg.removeChild(this.lines[len])
      }
      this.lines.length = 0
    },

    draw_line: function(pout, pin, offset) {
      var line = jm.graph_svg.c('path')
      line.setAttribute('stroke', this.opts.line_color)
      line.setAttribute('stroke-width', this.opts.line_width)
      line.setAttribute('fill', 'transparent')
      this.lines.push(line)
      this.e_svg.appendChild(line)
      this._line_to(line, pin.x + offset.x, pin.y + offset.y, pout.x + offset.x, pout.y + offset.y)
    },

    copy_to: function(dest_canvas_ctx, callback) {
      var img = new Image()
      img.onload = function() {
        dest_canvas_ctx.drawImage(img, 0, 0)
        !!callback && callback()
      }
      img.src = 'data:image/svg+xml;base64,' + btoa(new XMLSerializer().serializeToString(this.e_svg))
    },

    _bezier_to: function(path, x1, y1, x2, y2) {
      path.setAttribute('d', 'M' + x1 + ' ' + y1 + ' C ' + (x1 + (x2 - x1) * 2 / 3) + ' ' + y1 + ', ' + x1 + ' ' + y2 + ', ' + x2 + ' ' + y2)
    },

    _line_to: function(path, x1, y1, x2, y2) {
      path.setAttribute('d', 'M ' + x1 + ' ' + y1 + ' L ' + x2 + ' ' + y2)
    }
  }

  // view provider
  jm.view_provider = function(jm, options) {
    this.opts = options
    this.jm = jm
    this.layout = jm.layout

    this.container = null
    this.e_panel = null
    this.e_nodes = null

    this.size = { w: 0, h: 0 }

    this.selected_node = null
    this.editing_node = null

    this.graph = null
  }

  jm.view_provider.prototype = {
    init: function() {
      logger.debug('view.init')

      this.container = $i(this.opts.container) ? this.opts.container : $g(this.opts.container)
      if (!this.container) {
        logger.error('the options.view.container was not be found in dom')
        return
      }
      this.e_panel = $c('div')
      this.e_nodes = $c('jmnodes')
      this.e_editor = $c('input')

      this.graph = this.opts.engine.toLowerCase() === 'svg' ? new jm.graph_svg(this) : new jm.graph_canvas(this)

      this.e_panel.className = 'jsmind-inner'
      this.e_panel.tabIndex = 1
      this.e_panel.appendChild(this.graph.element())
      this.e_panel.appendChild(this.e_nodes)

      this.e_editor.className = 'jsmind-editor'
      this.e_editor.type = 'text'

      this.actualZoom = 1
      this.zoomStep = 0.1
      this.minZoom = 0.5
      this.maxZoom = 2

      var v = this
      jm.util.dom.add_event(this.e_editor, 'keydown', function(e) {
        var evt = e || event
        if (evt.keyCode == 13) {
          v.edit_node_end()
          evt.stopPropagation()
        }
      })
      jm.util.dom.add_event(this.e_editor, 'blur', function(e) {
        v.edit_node_end()
      })

      this.container.appendChild(this.e_panel)
    },

    add_event: function(obj, event_name, event_handle) {
      jm.util.dom.add_event(this.e_nodes, event_name, function(e) {
        var evt = e || event
        event_handle.call(obj, evt)
      })
    },

    get_binded_nodeid: function(element) {
      if (element == null) {
        return null
      }
      var tagName = element.tagName.toLowerCase()
      if (tagName == 'jmnodes' || tagName == 'body' || tagName == 'html') {
        return null
      }
      if (tagName == 'jmnode' || tagName == 'jmexpander') {
        return element.getAttribute('nodeid')
      } else {
        return this.get_binded_nodeid(element.parentElement)
      }
    },

    is_expander: function(element) {
      return (element.tagName.toLowerCase() == 'jmexpander')
    },

    reset: function() {
      logger.debug('view.reset')
      this.selected_node = null
      this.clear_lines()
      this.clear_nodes()
      this.reset_theme()
    },

    reset_theme: function() {
      var theme_name = this.jm.options.theme
      if (!!theme_name) {
        this.e_nodes.className = 'theme-' + theme_name
      } else {
        this.e_nodes.className = ''
      }
    },

    reset_custom_style: function() {
      var nodes = this.jm.mind.nodes
      for (var nodeid in nodes) {
        this.reset_node_custom_style(nodes[nodeid])
      }
    },

    load: function() {
      logger.debug('view.load')
      this.init_nodes()
    },

    expand_size: function() {
      var min_size = this.layout.get_min_size()
      var min_width = min_size.w + this.opts.hmargin * 2
      var min_height = min_size.h + this.opts.vmargin * 2
      var client_w = this.e_panel.clientWidth
      var client_h = this.e_panel.clientHeight
      if (client_w < min_width) {
        client_w = min_width
      }
      if (client_h < min_height) {
        client_h = min_height
      }
      this.size.w = client_w
      this.size.h = client_h
    },

    init_nodes_size: function(node) {
      var view_data = node._data.view
      view_data.width = view_data.element.clientWidth
      view_data.height = view_data.element.clientHeight
    },

    init_nodes: function() {
      var nodes = this.jm.mind.nodes
      var doc_frag = $d.createDocumentFragment()
      for (var nodeid in nodes) {
        this.create_node_element(nodes[nodeid], doc_frag)
      }
      this.e_nodes.appendChild(doc_frag)
      for (var nodeid in nodes) {
        this.init_nodes_size(nodes[nodeid])
      }
    },

    add_node: function(node) {
      this.create_node_element(node, this.e_nodes)
      this.init_nodes_size(node)
    },

    create_node_element: function(node, parent_node) {
      var view_data = null
      if ('view' in node._data) {
        view_data = node._data.view
      } else {
        view_data = {}
        node._data.view = view_data
      }

      var d = $c('jmnode')
      if (node.isroot) {
        d.className = 'root'
      } else {
        var d_e = $c('jmexpander')
        $t(d_e, '-')
        d_e.setAttribute('nodeid', node.id)
        d_e.style.visibility = 'hidden'
        parent_node.appendChild(d_e)
        view_data.expander = d_e
      }
      if (!!node.topic) {
        if (this.opts.support_html) {
          $h(d, node.topic)
        } else {
          $t(d, node.topic)
        }
      }
      d.setAttribute('nodeid', node.id)
      d.style.visibility = 'hidden'
      this._reset_node_custom_style(d, node.data)

      parent_node.appendChild(d)
      view_data.element = d
    },

    remove_node: function(node) {
      if (this.selected_node != null && this.selected_node.id == node.id) {
        this.selected_node = null
      }
      if (this.editing_node != null && this.editing_node.id == node.id) {
        node._data.view.element.removeChild(this.e_editor)
        this.editing_node = null
      }
      var children = node.children
      var i = children.length
      while (i--) {
        this.remove_node(children[i])
      }
      if (node._data.view) {
        var element = node._data.view.element
        var expander = node._data.view.expander
        this.e_nodes.removeChild(element)
        this.e_nodes.removeChild(expander)
        node._data.view.element = null
        node._data.view.expander = null
      }
    },

    update_node: function(node) {
      var view_data = node._data.view
      var element = view_data.element
      if (!!node.topic) {
        if (this.opts.support_html) {
          $h(element, node.topic)
        } else {
          $t(element, node.topic)
        }
      }
      view_data.width = element.clientWidth
      view_data.height = element.clientHeight
    },

    select_node: function(node) {
      if (!!this.selected_node) {
        this.selected_node._data.view.element.className =
          this.selected_node._data.view.element.className.replace(/\s*selected\b/i, '')
        this.reset_node_custom_style(this.selected_node)
      }
      if (!!node) {
        var type = node.data.type
        //点击事件
        if (type === 'device') {
          var device = node.data.device
          device.type = 'device'
          window.eventBus.$emit('deviceClick', device)
        } else if (type === 'gateway') {
          var gateway = node.data
          gateway.type = 'gateway'
          window.eventBus.$emit('deviceClick', gateway)
        } else if (type === 'gatewayChannel') {
          this.selected_node = node
          node._data.view.element.className += ' selected'
          this.clear_node_custom_style(node)
          var gatewayChannel = node.data
          gatewayChannel.type = 'gatewayChannel'
          window.eventBus.$emit('deviceClick', gatewayChannel)
        }
      }
    },

    click_node: function(node) {
      if (!!this.selected_node) {
        this.selected_node._data.view.element.className =
          this.selected_node._data.view.element.className.replace(/\s*selected\b/i, '')
        this.reset_node_custom_style(this.selected_node)
      }
      if (!!node) {
        this.selected_node = node
        node._data.view.element.className += ' selected'
        this.clear_node_custom_style(node)
        var type = node.data.type
        //点击事件
        if (type === 'device') {
          var device = node.data.device
          device.type = 'device'
          window.eventBus.$emit('deviceClick', device)
        } else if (type === 'gateway') {
          var gateway = node.data
          gateway.type = 'gateway'
          window.eventBus.$emit('deviceClick', gateway)
        } else if (type === 'gatewayChannel') {
          var gatewayChannel = node.data
          gatewayChannel.type = 'gatewayChannel'
          window.eventBus.$emit('deviceClick', gatewayChannel)
        }
      }
    },

    select_clear: function() {
      this.select_node(null)
    },

    get_editing_node: function() {
      return this.editing_node
    },

    is_editing: function() {
      return (!!this.editing_node)
    },

    edit_node_begin: function(node) {
      if (!node.topic) {
        logger.warn('don\'t edit image nodes')
        return
      }
      if (this.editing_node != null) {
        this.edit_node_end()
      }
      this.editing_node = node
      var view_data = node._data.view
      var element = view_data.element
      var topic = node.topic
      var ncs = getComputedStyle(element)
      this.e_editor.value = topic
      this.e_editor.style.width = (element.clientWidth - parseInt(ncs.getPropertyValue('padding-left')) - parseInt(ncs.getPropertyValue('padding-right'))) + 'px'
      element.innerHTML = ''
      element.appendChild(this.e_editor)
      element.style.zIndex = 5
      this.e_editor.focus()
      this.e_editor.select()
    },

    edit_node_end: function() {
      if (this.editing_node != null) {
        var node = this.editing_node
        this.editing_node = null
        var view_data = node._data.view
        var element = view_data.element
        var topic = this.e_editor.value
        element.style.zIndex = 'auto'
        element.removeChild(this.e_editor)
        if (jm.util.text.is_empty(topic) || node.topic === topic) {
          if (this.opts.support_html) {
            $h(element, node.topic)
          } else {
            $t(element, node.topic)
          }
        } else {
          this.jm.update_node(node.id, topic)
        }
      }
    },

    get_view_offset: function() {
      var bounds = this.layout.bounds
      var _x = (this.size.w - bounds.e - bounds.w) / 2
      var _y = this.size.h / 2
      return { x: _x, y: _y }
    },

    resize: function() {
      this.graph.set_size(1, 1)
      this.e_nodes.style.width = '1px'
      this.e_nodes.style.height = '1px'

      this.expand_size()
      this._show()
    },

    _show: function() {
      this.graph.set_size(this.size.w, this.size.h)
      this.e_nodes.style.width = this.size.w + 'px'
      this.e_nodes.style.height = this.size.h + 'px'
      this.show_nodes()
      this.show_lines()
      //this.layout.cache_valid = true;
      this.jm.invoke_event_handle(jm.event_type.resize, { data: [] })
    },

    zoomIn: function() {
      return this.setZoom(this.actualZoom + this.zoomStep)
    },

    zoomOut: function() {
      return this.setZoom(this.actualZoom - this.zoomStep)
    },

    setZoom: function(zoom) {
      if ((zoom < this.minZoom) || (zoom > this.maxZoom)) {
        return false
      }
      this.actualZoom = zoom
      for (var i = 0; i < this.e_panel.children.length; i++) {
        this.e_panel.children[i].style.transform = 'scale(' + zoom + ')'
      }

      this.show(true)
      return true

    },

    _center_root: function() {
      // center root node
      var outer_w = this.e_panel.clientWidth
      var outer_h = this.e_panel.clientHeight
      if (this.size.w > outer_w) {
        var _offset = this.get_view_offset()
        this.e_panel.scrollLeft = _offset.x - outer_w / 2
      }
      if (this.size.h > outer_h) {
        this.e_panel.scrollTop = (this.size.h - outer_h) / 2
      }
    },

    show: function(keep_center) {
      logger.debug('view.show')
      this.expand_size()
      this._show()
      if (!!keep_center) {
        this._center_root()
      }
    },

    relayout: function() {
      this.expand_size()
      this._show()
    },

    save_location: function(node) {
      var vd = node._data.view
      vd._saved_location = {
        x: parseInt(vd.element.style.left) - this.e_panel.scrollLeft,
        y: parseInt(vd.element.style.top) - this.e_panel.scrollTop
      }
    },

    restore_location: function(node) {
      var vd = node._data.view
      this.e_panel.scrollLeft = parseInt(vd.element.style.left) - vd._saved_location.x
      this.e_panel.scrollTop = parseInt(vd.element.style.top) - vd._saved_location.y
    },

    clear_nodes: function() {
      var mind = this.jm.mind
      if (mind == null) {
        return
      }
      var nodes = mind.nodes
      var node = null
      for (var nodeid in nodes) {
        node = nodes[nodeid]
        node._data.view.element = null
        node._data.view.expander = null
      }
      this.e_nodes.innerHTML = ''
    },

    show_nodes: function() {
      var nodes = this.jm.mind.nodes
      var node = null
      var node_element = null
      var expander = null
      var p = null
      var p_expander = null
      var expander_text = '-'
      var view_data = null
      var _offset = this.get_view_offset()
      for (var nodeid in nodes) {
        node = nodes[nodeid]
        view_data = node._data.view
        node_element = view_data.element
        expander = view_data.expander
        if (!this.layout.is_visible(node)) {
          node_element.style.display = 'none'
          expander.style.display = 'none'
          continue
        }
        this.reset_node_custom_style(node)
        p = this.layout.get_node_point(node)
        view_data.abs_x = _offset.x + p.x
        view_data.abs_y = _offset.y + p.y
        node_element.style.left = (_offset.x + p.x) + 'px'
        node_element.style.top = (_offset.y + p.y) + 'px'
        node_element.style.display = ''
        node_element.style.visibility = 'visible'
        if (!node.isroot && node.children.length > 0) {
          expander_text = node.expanded ? '-' : '+'
          p_expander = this.layout.get_expander_point(node)
          expander.style.left = (_offset.x + p_expander.x) + 'px'
          expander.style.top = (_offset.y + p_expander.y) + 'px'
          expander.style.display = ''
          expander.style.visibility = 'visible'
          $t(expander, expander_text)
        }
        // hide expander while all children have been removed
        if (!node.isroot && node.children.length == 0) {
          expander.style.display = 'none'
          expander.style.visibility = 'hidden'
        }
      }
    },

    reset_node_custom_style: function(node) {
      this._reset_node_custom_style(node._data.view.element, node.data)
    },

    _reset_node_custom_style: function(node_element, node_data) {
      if ('background-color' in node_data) {
        node_element.style.backgroundColor = node_data['background-color']
      } else {
        node_element.style.backgroundColor = '#0c3039'
        node_element.classList.add('border')
      }
      if ('title' in node_data) {
        node_element.setAttribute('title', node_data['title'])
      }
      if ('channel_type' in node_data) {
        node_element.setAttribute('channel-type', node_data['channel_type'])
      }
      if ('channel' in node_data) {
        node_element.setAttribute('channel', node_data['channel'])
        node_element.setAttribute('title', 'CH_'+node_data['title'])
      }
      if ('foreground-color' in node_data) {
        node_element.style.color = node_data['foreground-color']
      }
      if ('width' in node_data) {
        node_element.style.width = node_data['width'] + 'px'
      }
      if ('height' in node_data) {
        node_element.style.height = node_data['height'] + 'px'
      }
      if ('font-size' in node_data) {
        node_element.style.fontSize = node_data['font-size'] + 'px'
      }
      if ('font-weight' in node_data) {
        node_element.style.fontWeight = node_data['font-weight']
      }
      if ('font-style' in node_data) {
        node_element.style.fontStyle = node_data['font-style']
      }
      if ('background-image' in node_data) {
        var backgroundImage = node_data['background-image']
        if (backgroundImage.startsWith('data') && node_data['width'] && node_data['height']) {
          var img = new Image()

          img.onload = function() {
            var c = $c('canvas')
            c.width = node_element.clientWidth
            c.height = node_element.clientHeight
            var img = this
            if (c.getContext) {
              var ctx = c.getContext('2d')
              ctx.drawImage(img, 2, 2, node_element.clientWidth, node_element.clientHeight)
              var scaledImageData = c.toDataURL()
              node_element.style.backgroundImage = 'url(' + scaledImageData + ')'
            }
          }
          img.src = backgroundImage

        } else {
          node_element.style.backgroundImage = 'url(' + backgroundImage + ')'
        }
        node_element.style.backgroundSize = '99%'

        if ('background-rotation' in node_data) {
          node_element.style.transform = 'rotate(' + node_data['background-rotation'] + 'deg)'
        }
      }
    },

    clear_node_custom_style: function(node) {
      var node_element = node._data.view.element
      node_element.style.backgroundColor = ''
      node_element.style.color = ''
    },

    clear_lines: function() {
      this.graph.clear()
    },

    show_lines: function() {
      this.clear_lines()
      var nodes = this.jm.mind.nodes
      var node = null
      var pin = null
      var pout = null
      var _offset = this.get_view_offset()
      for (var nodeid in nodes) {
        node = nodes[nodeid]
        if (!!node.isroot) {
          continue
        }
        if (('visible' in node._data.layout) && !node._data.layout.visible) {
          continue
        }
        pin = this.layout.get_node_point_in(node)
        pout = this.layout.get_node_point_out(node.parent)
        this.graph.draw_line(pout, pin, _offset)
      }
    }
  }

  // shortcut provider
  jm.shortcut_provider = function(jm, options) {
    this.jm = jm
    this.opts = options
    this.mapping = options.mapping
    this.handles = options.handles
    this._newid = null
    this._mapping = {}
  }

  jm.shortcut_provider.prototype = {
    init: function() {
      jm.util.dom.add_event(this.jm.view.e_panel, 'keydown', this.handler.bind(this))

      this.handles['addchild'] = this.handle_addchild
      this.handles['addbrother'] = this.handle_addbrother
      this.handles['editnode'] = this.handle_editnode
      this.handles['delnode'] = this.handle_delnode
      this.handles['toggle'] = this.handle_toggle
      this.handles['up'] = this.handle_up
      this.handles['down'] = this.handle_down
      this.handles['left'] = this.handle_left
      this.handles['right'] = this.handle_right

      for (var handle in this.mapping) {
        if (!!this.mapping[handle] && (handle in this.handles)) {
          this._mapping[this.mapping[handle]] = this.handles[handle]
        }
      }

      if (typeof this.opts.id_generator === 'function') {
        this._newid = this.opts.id_generator
      } else {
        this._newid = jm.util.uuid.newid
      }
    },

    enable_shortcut: function() {
      this.opts.enable = true
    },

    disable_shortcut: function() {
      this.opts.enable = false
    },

    handler: function(e) {
      if (e.which == 9) {
        e.preventDefault()
      } //prevent tab to change focus in browser
      if (this.jm.view.is_editing()) {
        return
      }
      var evt = e || event
      if (!this.opts.enable) {
        return true
      }
      var kc = evt.keyCode + (evt.metaKey << 13) + (evt.ctrlKey << 12) + (evt.altKey << 11) + (evt.shiftKey << 10)
      if (kc in this._mapping) {
        this._mapping[kc].call(this, this.jm, e)
      }
    },

    handle_addchild: function(_jm, e) {
      var selected_node = _jm.get_selected_node()
      if (!!selected_node) {
        var nodeid = this._newid()
        var node = _jm.add_node(selected_node, nodeid, 'New Node')
        if (!!node) {
          _jm.select_node(nodeid)
          _jm.begin_edit(nodeid)
        }
      }
    },
    handle_addbrother: function(_jm, e) {
      var selected_node = _jm.get_selected_node()
      if (!!selected_node && !selected_node.isroot) {
        var nodeid = this._newid()
        var node = _jm.insert_node_after(selected_node, nodeid, 'New Node')
        if (!!node) {
          _jm.select_node(nodeid)
          _jm.begin_edit(nodeid)
        }
      }
    },
    handle_editnode: function(_jm, e) {
      var selected_node = _jm.get_selected_node()
      if (!!selected_node) {
        _jm.begin_edit(selected_node)
      }
    },
    handle_delnode: function(_jm, e) {
      var selected_node = _jm.get_selected_node()
      if (!!selected_node && !selected_node.isroot) {
        _jm.select_node(selected_node.parent)
        _jm.remove_node(selected_node)
      }
    },
    handle_toggle: function(_jm, e) {
      var evt = e || event
      var selected_node = _jm.get_selected_node()
      if (!!selected_node) {
        _jm.toggle_node(selected_node.id)
        evt.stopPropagation()
        evt.preventDefault()
      }
    },
    handle_up: function(_jm, e) {
      var evt = e || event
      var selected_node = _jm.get_selected_node()
      if (!!selected_node) {
        var up_node = _jm.find_node_before(selected_node)
        if (!up_node) {
          var np = _jm.find_node_before(selected_node.parent)
          if (!!np && np.children.length > 0) {
            up_node = np.children[np.children.length - 1]
          }
        }
        if (!!up_node) {
          _jm.select_node(up_node)
        }
        evt.stopPropagation()
        evt.preventDefault()
      }
    },

    handle_down: function(_jm, e) {
      var evt = e || event
      var selected_node = _jm.get_selected_node()
      if (!!selected_node) {
        var down_node = _jm.find_node_after(selected_node)
        if (!down_node) {
          var np = _jm.find_node_after(selected_node.parent)
          if (!!np && np.children.length > 0) {
            down_node = np.children[0]
          }
        }
        if (!!down_node) {
          _jm.select_node(down_node)
        }
        evt.stopPropagation()
        evt.preventDefault()
      }
    },

    handle_left: function(_jm, e) {
      this._handle_direction(_jm, e, jm.direction.left)
    },
    handle_right: function(_jm, e) {
      this._handle_direction(_jm, e, jm.direction.right)
    },
    _handle_direction: function(_jm, e, d) {
      var evt = e || event
      var selected_node = _jm.get_selected_node()
      var node = null
      if (!!selected_node) {
        if (selected_node.isroot) {
          var c = selected_node.children
          var children = []
          for (var i = 0; i < c.length; i++) {
            if (c[i].direction === d) {
              children.push(i)
            }
          }
          node = c[children[Math.floor((children.length - 1) / 2)]]
        } else if (selected_node.direction === d) {
          var children = selected_node.children
          var childrencount = children.length
          if (childrencount > 0) {
            node = children[Math.floor((childrencount - 1) / 2)]
          }
        } else {
          node = selected_node.parent
        }
        if (!!node) {
          _jm.select_node(node)
        }
        evt.stopPropagation()
        evt.preventDefault()
      }
    }
  }


  // plugin
  jm.plugin = function(name, init) {
    this.name = name
    this.init = init
  }

  jm.plugins = []

  jm.register_plugin = function(plugin) {
    if (plugin instanceof jm.plugin) {
      jm.plugins.push(plugin)
    }
  }

  jm.init_plugins = function(sender) {
    $w.setTimeout(function() {
      jm._init_plugins(sender)
    }, 0)
  }

  jm._init_plugins = function(sender) {
    var l = jm.plugins.length
    var fn_init = null
    for (var i = 0; i < l; i++) {
      fn_init = jm.plugins[i].init
      if (typeof fn_init === 'function') {
        fn_init(sender)
      }
    }
  }

  // quick way
  jm.show = function(options, mind) {
    var _jm = new jm(options)
    _jm.show(mind)
    return _jm
  }

  // export jsmind
  if (typeof module !== 'undefined' && typeof exports === 'object') {
    module.exports = jm
  } else if (typeof define === 'function' && (define.amd || define.cmd)) {
    define(function() {
      return jm
    })
  } else {
    $w[__name__] = jm
  }
})(typeof window !== 'undefined' ? window : global)


